Перейти к основному содержимому

5.05. Обобщения

Разработчику Архитектору

Обобщения

Обобщения (generics) — это механизм языка C#, позволяющий определять классы, методы, интерфейсы, делегаты и структуры без указания конкретного типа данных. Вместо этого используется параметр типа, который заменяется на реальный тип во время использования.

List<int> numbers = new List<int>();
Dictionary<string, Person> people = new Dictionary<string, Person>();

Здесь int, string, Person — конкретные типы, подставляемые вместо параметров T, TKey, TValue.

Обобщённый тип — это тип (класс, структура, интерфейс и т.д.), который объявлен с одним или несколькими параметрами типа. Эти параметры — «заглушки», которые будут заменены на реальные типы при создании экземпляра.

public class Box<T>
{
public T Content { get; set; }
}

Здесь T — параметр типа. Когда ты создаёшь Box<int>, T становится int.

Зачем это нужно?

Во-первых, типобезопасность - нет приведений типов (cast), нет ошибок времени выполнения из-за невыполнимых типов.

Во-вторых, производительность, для значимых типов нет упаковки-распаковки, что критично для производительности.

В-третьих, повторное использование кода - один класс List<T> работает с int, string, Person, DateTime и любыми другими типами.

При создании библиотек, которые должны работать с любыми типами, при работе с коллекциями, где нужна типобезопасность и производительность, при реализации алгоритмов, независимых от конкретного типа данных используются обобщенные типы.

Примеры:

List<T>          // Список элементов любого типа T
Dictionary<K, V> // Словарь с ключами K и значениями V
Stack<T> // Стек из элементов типа T

Параметры типа — это идентификаторы, используемые как «переменные для типов». Хотя можно использовать любые имена, существуют общепринятые соглашения:

Буквы эти не имеют значения сами по себе, это просто соглашения:

  • T – Type (тип – любой);
  • K – Key (ключ в словаре);
  • V – Value (значение в словаре);
  • U, S – дополнительные типы, если нужно несколько (второй и третий произвольный тип);
  • TResult – результат метода или функции;
  • TKey и TValue – то же самое, что и K и V.

Соответственно, можно использовать любые имена:

Dictionary<TKey, TValue>
KeyValuePair<TKey, TValue>
Func<T, TResult>
Action<T1, T2>

public class Box<Apple> { ... }

Пример:

List<int> numbers = new List<int>();
numbers.Add(10);
int x = numbers[0]; // Нет необходимости приводить к int

То же самое можно сделать со string, Person, DateTime и т.д.:

List<string> names = new List<string>();
names.Add("Alice");
string name = names[0];

Вместо того, чтобы писать отдельный класс ListOfInt, ListOfString, ListOfPerson и так далее – мы используем один универсальный класс List<T>.

Обобщённые классы.

Обобщённый класс — это класс, принимающий тип в качестве параметра.

public class Box<T>
{
public T Item { get; set; }
}

Пример – создаём класс Box<T>:

public class Box<T>
{
public T Content { get; set; }

public void Show()
{
Console.WriteLine(Content);
}
}

Использование:

Box<int> box1 = new Box<int>();
box1.Content = 42;
box1.Show(); // вывод: 42

Box<string> box2 = new Box<string>();
box2.Content = "Hello";
box2.Show(); // вывод: Hello

Каждый экземпляр — типобезопасен. Нельзя положить string в Box<int>.

Обобщённые методы.

Методы тоже могут быть обобщёнными — они определяют свои параметры типа.

public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}

Пример – создаём метод T Max<T>(T a, T b) where T : IComparable<T>:

public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}

Использование:

int maxInt = Max(10, 20); // 20
string maxStr = Max("apple", "banana"); // banana

Здесь компилятор сам определяет тип T. Компилятор выводит тип автоматически — не нужно писать Max<int>(5, 10).

Без where T : IComparable<T> — CompareTo не будет доступен.

Иногда обобщённому коду нужно знать что-то о типе T, чтобы вызывать методы, создавать экземпляры и т.д. Для этого используются ограничения (where).

Чтобы обобщённый код мог выполнять операции над типом T, нужно иногда указывать ограничения. Ограничения типов указывают, какие типы могут использоваться в качестве аргумента:

  • where T : class – T должен быть ссылочным типом;
  • where T : struct – T должен быть значимым типом;
  • where T : SomeBaseClass – T должен наследоваться от SomeBaseClass;
  • where T : ISomeInterface – T должен реализовывать интерфейс ISomeInterface;
  • where T : new() – должен иметь конструктор без параметров;
  • where T : unmanaged - неуправляемый тип (без сборки мусора);
  • where T : notnull - не может быть null;
  • where T : U - должен быть совместим с U.

Пример: класс с ограничениями

public class Service<T> where T : class, new()
{
public T CreateInstance()
{
return new T(); // Работает благодаря new()
}
}

Без new() нельзя писать new T(). Чтобы правильно использовано нужно добавить именно where T: new().